写在最前面

这一篇文章其实是计划之外的,因为一开始没想要深究反射效率的问题。但是如果翻了 ButterKnife 项目文件结构的时候,会看到如下结构:

.
├── build.gradle
├── butterknife
│   ├── build
│   ├── build.gradle
│   ├── butterknife.butterknife.iml
│   ├── gradle.properties
│   ├── proguard-rules.txt
│   └── src
├── butterknife-annotations
│   ├── build
│   ├── build.gradle
│   ├── butterknife-annotations.iml
│   ├── gradle.properties
│   └── src
├── butterknife-compiler
│   ├── build
│   ├── build.gradle
│   ├── butterknife-compiler.iml
│   ├── gradle.properties
│   └── src
├── butterknife-gradle-plugin
│   ├── build.gradle
│   ├── butterknife-gradle-plugin.iml
│   ├── gradle.properties
│   └── src
├── butterknife-reflect
│   ├── README.md
│   ├── build
│   ├── build.gradle
│   ├── butterknife-reflect.iml
│   ├── gradle.properties
│   ├── proguard-rules.txt
│   └── src
├── butterknife-runtime
│   ├── build
│   ├── build.gradle
│   ├── butterknife-runtime.iml
│   ├── gradle.properties
│   └── src
├── gradle
│   ├── gradle-mvn-push.gradle
│   └── wrapper
├── gradle.properties
├── gradlew
├── gradlew.bat
├── local.properties
├── sample
│   ├── app
│   ├── library
│   └── sample.iml
├── settings.gradle
└── website

核心的工程库有这个几个:butterknifebutterknife-annotationsbutterknife-compilerbutterknife-gradle-pluginbutterknife-reflectbutterknife-runtimesample.appsample.library 这个几个。其中依赖关系如下:

这里面只有 butterknife-runtime 库,没有在引用链中。而其实这个库有相应的 README,简而言之来说就是:

通常的 butterknife 使用编译时注解,生成中间文件,这样有更好的性能。但是 butterknife-runtime 是一个完全运行时的库,通过反射来实现的,性能会差。

那么问题来了,为啥反射的时候性能会差?本篇我们来大致看看。本篇所有的 Java 源码分析都是用的 1.8 的源码。

获取方法

上篇中,我们已经说过反射获取一个 Method 对象是怎样的:

Method getMethod(String name, Class<?>... parameterTypes);

Method getDeclaredMethod(String name, Class<?>... parameterTypes);

我们知道第一个方法是获取 自己 和 父类 所有 public 方法,第二个是获取自己所有方法,那我们就分开来看看。

getMethod

public Method getMethod(String name, Class<?>... parameterTypes)
    throws NoSuchMethodException, SecurityException {
    //校验权限
    checkMemberAccess(Member.PUBLIC, Reflection.getCallerClass(), true);
    //获取方法
    Method method = getMethod0(name, parameterTypes, true);
    if (method == null) {
        throw new NoSuchMethodException(getName() + "." + name + argumentTypesToString(parameterTypes));
    }
    return method;
}

简单粗暴,先校验权限后获取方法。我们直接看获取方法的相关内容:

private Method getMethod0(String name, Class<?>[] parameterTypes, boolean includeStaticMethods) {
    MethodArray interfaceCandidates = new MethodArray(2);
    Method res =  privateGetMethodRecursive(name, parameterTypes, includeStaticMethods, interfaceCandidates);
    if (res != null)
        return res;

    // Not found on class or superclass directly
    interfaceCandidates.removeLessSpecifics();
    return interfaceCandidates.getFirst(); // may be null
}

分了两步来判断,如果尝试获取method,如果存在就直接返回。如果没有,对接口类型进行查重返回。

看下 privateGetMethodRecursive 方法:


private Method privateGetMethodRecursive(String name,
        Class<?>[] parameterTypes,
        boolean includeStaticMethods,
        MethodArray allInterfaceCandidates) {

    Method res;
    // Search declared public methods
    if ((res = searchMethods(privateGetDeclaredMethods(true),
                                name,
                                parameterTypes)) != null) {
        if (includeStaticMethods || !Modifier.isStatic(res.getModifiers()))
            return res;
    }
    // Search superclass's methods
    if (!isInterface()) {
        Class<? super T> c = getSuperclass();
        if (c != null) {
            if ((res = c.getMethod0(name, parameterTypes, true)) != null) {
                return res;
            }
        }
    }
    // Search superinterfaces' methods
    Class<?>[] interfaces = getInterfaces();
    for (Class<?> c : interfaces)
        if ((res = c.getMethod0(name, parameterTypes, false)) != null)
            allInterfaceCandidates.add(res);
    // Not found
    return null;
}

分了三种情况:

- 查找自己的 public 方法
- 如果不是接口,同时有父类,查找父类的的方法,递归回到 getMethod0
- 如果是接口,查找接口的方法,并放到候选数组中。

先看第一个

// 获取 declaredPublicMethods,单例模式,懒启动。
private Method[] privateGetDeclaredMethods(boolean publicOnly) {
    checkInitted();
    Method[] res;
    ReflectionData<T> rd = reflectionData();
    if (rd != null) {
        res = publicOnly ? rd.declaredPublicMethods : rd.declaredMethods;
        if (res != null) return res;
    }
    // No cached value available; request value from VM
    res = Reflection.filterMethods(this, getDeclaredMethods0(publicOnly));
    if (rd != null) {
        if (publicOnly) {
            rd.declaredPublicMethods = res;
        } else {
            rd.declaredMethods = res;
        }
    }
    return res;
}


//校验函数签名是否 完全一直
private static Method searchMethods(Method[] methods,
                                    String name,
                                    Class<?>[] parameterTypes)
{
    Method res = null;
    String internedName = name.intern();
    for (int i = 0; i < methods.length; i++) {
        Method m = methods[i];
        if (m.getName() == internedName
            && arrayContentsEq(parameterTypes, m.getParameterTypes())
            && (res == null
                || res.getReturnType().isAssignableFrom(m.getReturnType())))
            res = m;
    }

    return (res == null ? res : getReflectionFactory().copyMethod(res));
}

这样自己的 public 方法都已经找到。注意最后返回的是一个 method 的拷贝。这里拷贝最终调用的就是 Method.copy 方法,所有copy出来的 method 和原 method 共用同一个 MethodAccessor。

2,3 流程是类似的。不过如果单单是一个接口的话,会有个去重的操作,猜测大概是 由于子类和父类有相同的签名的函数吧。

接下来说下 getDeclaredMethod

public Method getDeclaredMethod(String name, Class<?>... parameterTypes)
    throws NoSuchMethodException, SecurityException {
    checkMemberAccess(Member.DECLARED, Reflection.getCallerClass(), true);
    Method method = searchMethods(privateGetDeclaredMethods(false), name, parameterTypes);
    if (method == null) {
        throw new NoSuchMethodException(getName() + "." + name + argumentTypesToString(parameterTypes));
    }
    return method;
}

直接调用了 searchMethods,直接什么都不看,就看这个类自己的 method。返回查找结果。

因此 一个Method的获取过程大致是这样的:

getMethod{
    checkMemberAccess()
    getMethod0{
        privateGetMethodRecursive{
            // 查找本身
            searchMethods(Method.PUBLIC)

            // 查找父类
            getSuperclass().getMethod0()

            // 查找接口
            getInterfaces().getMethod0()
        }
    }
}

调用方法

Method 之前我们说调用方法为 Method#invoke,那我们直接来看看这里的思路:

public Object invoke(Object obj, Object... args)
    throws IllegalAccessException, IllegalArgumentException,
        InvocationTargetException
{
    // accessableObject 的熟悉,setAccessable 时会设置这里的属性,否则要查看其权限。
    if (!override) {
        if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
            Class<?> caller = Reflection.getCallerClass();
            checkAccess(caller, clazz, obj, modifiers);
        }
    }
    MethodAccessor ma = methodAccessor;             // read volatile

    // 获取 MethodAccessor,由其来真正进行调用
    if (ma == null) {
        ma = acquireMethodAccessor();
    }
    return ma.invoke(obj, args);
}

private MethodAccessor acquireMethodAccessor() {
    // First check to see if one has been created yet, and take it
    // if so
    MethodAccessor tmp = null;
    if (root != null) tmp = root.getMethodAccessor();
    if (tmp != null) {
        methodAccessor = tmp;
    } else {
        // Otherwise fabricate one and propagate it up to the root
        tmp = reflectionFactory.newMethodAccessor(this);
        setMethodAccessor(tmp);
    }

    return tmp;
}

public MethodAccessor newMethodAccessor(Method var1) {
    checkInitted();
    if (noInflation && !ReflectUtil.isVMAnonymousClass(var1.getDeclaringClass())) {
        return (new MethodAccessorGenerator())
            .generateMethod(var1.getDeclaringClass(), 
                            var1.getName(), 
                            var1.getParameterTypes(), 
                            var1.getReturnType(), 
                            var1.getExceptionTypes(), 
                            var1.getModifiers());
    } else {
        NativeMethodAccessorImpl var2 = new NativeMethodAccessorImpl(var1);
        DelegatingMethodAccessorImpl var3 = new DelegatingMethodAccessorImpl(var2);
        var2.setParent(var3);
        return var3;
    }
}

这里可以看到,一共有三种 MethodAccessor。MethodAccessorImplNativeMethodAccessorImplDelegatingMethodAccessorImpl。 采用哪种 MethodAccessor 根据 noInflation 进行判断,noInflation 默认值为 false,只有指定了 sun.reflect.noInflation 属性为 true,才会 采用 MethodAccessorImpl。 所以默认会调用 NativeMethodAccessorImpl。

而在 NativeMethodAccessorImpl 中,invoke 方法调用如下:

    public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
        if (++this.numInvocations > ReflectionFactory.inflationThreshold() 
                && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {
            MethodAccessorImpl var3 = (MethodAccessorImpl)(new MethodAccessorGenerator())
                .generateMethod(this.method.getDeclaringClass(), 
                                this.method.getName(), 
                                this.method.getParameterTypes(), 
                                this.method.getReturnType(), 
                                this.method.getExceptionTypes(), 
                                this.method.getModifiers());
            this.parent.setDelegate(var3);
        }

        return invoke0(this.method, var1, var2);
    }

所以当调用次数 大于 ReflectionFactory.inflationThreshold 时会使用 Java 的 MethodAccessorImpl,否则会使用 NativeMethodAccessorImpl。这是由于:

Java 版本的 MethodAccessorImpl 调用效率比 Native 版本要快 20 倍以上,但是 Java 版本加载时要比 Native 多消耗 3-4 倍资源,所以默认会调用 Native 版本,如果调用次数超过 15 次以后,就会选择运行效率更高的 Java 版本。

小结

了解了反射的原理以后,我们来分析一下反射效率低的原因。

Method#invoke 方法会对参数做封装和解封操作

我们可以看到,invoke 方法的参数是 Object[] 类型,也就是说,如果方法参数是简单类型的话,需要在此转化成 Object 类型,例如 long ,在 javac compile 的时候 用了Long.valueOf() 转型,也就大量了生成了Long 的 Object, 同时 传入的参数是Object[]数值,那还需要额外封装object数组。 而在上面 MethodAccessorGenerator#emitInvoke 方法里我们看到,生成的字节码时,会把参数数组拆解开来,把参数恢复到没有被 Object[] 包装前的样子,同时还要对参数做校验,这里就涉及到了解封操作。 因此,在反射调用的时候,因为封装和解封,产生了额外的不必要的内存浪费,当调用次数达到一定量的时候,还会导致 GC。

需要检查方法可见性

通过上面的源码分析,我们会发现,反射时每次调用都必须检查方法的可见性(在 Method.invoke 里)

需要校验参数

反射时也必须检查每个实际参数与形式参数的类型匹配性(在NativeMethodAccessorImpl.invoke0 里或者生成的 Java 版 MethodAccessor.invoke 里);

反射方法难以内联

Method#invoke 就像是个独木桥一样,各处的反射调用都要挤过去,在调用点上收集到的类型信息就会很乱,影响内联程序的判断,使得 Method.invoke() 自身难以被内联到调用方。参见 https://www.iteye.com/blog/rednaxelafx-548536

JIT 无法优化

在 JavaDoc 中提到:

Because reflection involves types that are dynamically resolved, certain Java virtual machine optimizations can not be performed. Consequently, reflective operations have slower performance than their non-reflective counterparts, and should be avoided in sections of code which are called frequently in performance-sensitive applications.

因为反射涉及到动态加载的类型,所以无法进行优化。

ok,本篇只是粗浅的分析了 反射 效率为什么比较慢,很多内容参考了:https://github.com/5A59/android-training/blob/master/jvm-art/java%E5%8F%8D%E5%B0%84%E4%B8%BA%E4%BB%80%E4%B9%88%E6%85%A2.md 这篇文章,在此表示感谢。

results matching ""

    No results matching ""